#include "note_dialog.h"
#include "app_frame.h"
#include "util.h"
#include "unicode.h"
#include "settings.h"
#include "text_layout.h"
#include <wx/choicdlg.h>

//(*InternalHeaders(NoteDialog)
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <wx/intl.h>
#include <wx/button.h>
#include <wx/string.h>
//*)

using std::max;
using std::min;

//(*IdInit(NoteDialog)
const long NoteDialog::ID_STATICTEXT1 = wxNewId();
const long NoteDialog::ID_CAPTION = wxNewId();
const long NoteDialog::ID_STATICTEXT2 = wxNewId();
const long NoteDialog::ID_DATE = wxNewId();
const long NoteDialog::ID_STATICTEXT3 = wxNewId();
const long NoteDialog::ID_MANUAL_KEYWORDS = wxNewId();
const long NoteDialog::ID_STATICTEXT4 = wxNewId();
const long NoteDialog::ID_TEXT = wxNewId();
const long NoteDialog::ID_INSERT_SOURCE = wxNewId();
const long NoteDialog::ID_INDIRECT_SOURCE = wxNewId();
const long NoteDialog::ID_INSERT_KEYWORD = wxNewId();
const long NoteDialog::ID_MARK_KEYWORD = wxNewId();
const long NoteDialog::ID_RESET = wxNewId();
//*)

BEGIN_EVENT_TABLE(NoteDialog,wxPanel)
	//(*EventTable(NoteDialog)
	//*)
END_EVENT_TABLE()

NoteDialog::NoteDialog(wxWindow* parent, AppFrame *top_frame, const Note& note)
{
    this->note = note;
    this->top_frame = top_frame;
    this->last_text_len = 0;
    this->updating_text_style = false;

	//(*Initialize(NoteDialog)
	wxFlexGridSizer* fields_sizer;
	wxBoxSizer* buttons_sizer;

	Create(parent, wxID_ANY, wxDefaultPosition, wxSize(528,209), 0, _T("wxID_ANY"));
	top_sizer = new wxBoxSizer(wxVERTICAL);
	fields_sizer = new wxFlexGridSizer(0, 2, 0, 0);
	fields_sizer->AddGrowableCol(1);
	caption_label = new wxStaticText(this, ID_STATICTEXT1, _("&1. Caption"), wxDefaultPosition, wxDefaultSize, 0, _T("ID_STATICTEXT1"));
	fields_sizer->Add(caption_label, 0, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	caption_box = new wxTextCtrl(this, ID_CAPTION, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator, _T("ID_CAPTION"));
	caption_box->SetFocus();
	caption_box->SetToolTip(_("Note caption\n\nA short summary of contents of note"));
	fields_sizer->Add(caption_box, 1, wxALL|wxEXPAND|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	date_label = new wxStaticText(this, ID_STATICTEXT2, _("&2. Date"), wxDefaultPosition, wxDefaultSize, 0, _T("ID_STATICTEXT2"));
	fields_sizer->Add(date_label, 0, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	date_box = new wxTextCtrl(this, ID_DATE, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator, _T("ID_DATE"));
	date_box->SetToolTip(_("Date for which this note is relevant. This allows you to filter only notes from certain date range during search.\n\nTODO: Dates, ISO / EU, incomplete date"));
	fields_sizer->Add(date_box, 1, wxALL|wxEXPAND|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	keywords_label = new wxStaticText(this, ID_STATICTEXT3, _("&3. Manual keywords"), wxDefaultPosition, wxDefaultSize, 0, _T("ID_STATICTEXT3"));
	fields_sizer->Add(keywords_label, 1, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	manual_keywords_box = new wxTextCtrl(this, ID_MANUAL_KEYWORDS, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE, wxDefaultValidator, _T("ID_MANUAL_KEYWORDS"));
	manual_keywords_box->SetMinSize(wxSize(50,50));
	manual_keywords_box->SetToolTip(_("Here you can list keywords, that you don\'t want to be displayed."));
	fields_sizer->Add(manual_keywords_box, 1, wxALL|wxEXPAND|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	text_label = new wxStaticText(this, ID_STATICTEXT4, _("&4. Text"), wxDefaultPosition, wxDefaultSize, 0, _T("ID_STATICTEXT4"));
	fields_sizer->Add(text_label, 1, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	text_box = new wxTextCtrl(this, ID_TEXT, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE|wxTE_RICH, wxDefaultValidator, _T("ID_TEXT"));
	text_box->SetMinSize(wxSize(50,50));
	text_box->SetToolTip(_("This is the text of the note. Apart from normal text, it allows several specially formated fields.\n\n- Citation of a source. It must be enclosed in (parentheses), and conform to patterns specified in TODO. You can use the \'Insert source\' or \'Insert indirect source\' to easily insert this field.\n\n- Keyword, enclosed in {braces}. Note can be searched  This will be highlighted \n\n- Marked keyword. Any 1-3 words can be marked as refering to a keyword. Keyword in {brackets} should follow those words, and should be prefixed with 1-3 \'<\' characters (depending on number of words to be marked). For example text \"G. Bush {<<George Walker Bush}\", will be displayed as \"G. Bush\", but it will refer to keyword \"George Walker Bush\". Use \'Mark keyword\' button to mark predefined keywords easily."));
	fields_sizer->Add(text_box, 1, wxALL|wxEXPAND|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	top_sizer->Add(fields_sizer, 1, wxALL|wxEXPAND|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	buttons_sizer = new wxBoxSizer(wxHORIZONTAL);
	insert_source_button = new wxButton(this, ID_INSERT_SOURCE, _("&Insert source"), wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator, _T("ID_INSERT_SOURCE"));
	insert_source_button->SetToolTip(_("Inserts citation of current source. If no current source is set, lets you set it first."));
	buttons_sizer->Add(insert_source_button, 0, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	insert_indirect_source_button = new wxButton(this, ID_INDIRECT_SOURCE, _("Insert in&direct source"), wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator, _T("ID_INDIRECT_SOURCE"));
	insert_indirect_source_button->SetToolTip(_("Sometimes current source refers to some other source. This lets you type the other source, and then enters \"indirect citation\" of other source via current source."));
	buttons_sizer->Add(insert_indirect_source_button, 0, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	insert_keyword_button = new wxButton(this, ID_INSERT_KEYWORD, _("Insert &keyword"), wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator, _T("ID_INSERT_KEYWORD"));
	insert_keyword_button->SetToolTip(_("Lets you type a keyword, and inserts it into text enclosed in {braces}. This is especially useful for keyboard layouts, that don\'t have braces."));
	buttons_sizer->Add(insert_keyword_button, 0, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	mark_keyword_button = new wxButton(this, ID_MARK_KEYWORD, _("&Mark keyword"), wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator, _T("ID_MARK_KEYWORD"));
	mark_keyword_button->SetToolTip(_("Look up keyword(s), whose hints match preceding 1-3 words. If such keyword is found, mark words as referring to it.\n\nThe longest match (most words) takes priority.\n\nIf there are multiple keywords matching, you can select one of them."));
	buttons_sizer->Add(mark_keyword_button, 0, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	buttons_sizer->Add(-1,-1,1, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	save_button = new wxButton(this, wxID_SAVE, _("&Save"), wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator, _T("wxID_SAVE"));
	buttons_sizer->Add(save_button, 0, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	cancel_button = new wxButton(this, wxID_CANCEL, _("&Cancel"), wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator, _T("wxID_CANCEL"));
	buttons_sizer->Add(cancel_button, 0, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	reset_button = new wxButton(this, ID_RESET, _("&Reset"), wxDefaultPosition, wxDefaultSize, 0, wxDefaultValidator, _T("ID_RESET"));
	reset_button->SetToolTip(_("Blank all fields of this dialog."));
	buttons_sizer->Add(reset_button, 0, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	top_sizer->Add(buttons_sizer, 0, wxALL|wxEXPAND|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5);
	SetSizer(top_sizer);
	SetSizer(top_sizer);
	Layout();

	Connect(ID_DATE,wxEVT_COMMAND_TEXT_UPDATED,(wxObjectEventFunction)&NoteDialog::OnDateModified);
	Connect(ID_MANUAL_KEYWORDS,wxEVT_COMMAND_TEXT_UPDATED,(wxObjectEventFunction)&NoteDialog::OnKeywordsModified);
	Connect(ID_TEXT,wxEVT_COMMAND_TEXT_UPDATED,(wxObjectEventFunction)&NoteDialog::OnTextChanged);
	Connect(ID_INSERT_SOURCE,wxEVT_COMMAND_BUTTON_CLICKED,(wxObjectEventFunction)&NoteDialog::OnInsertSource);
	Connect(ID_INDIRECT_SOURCE,wxEVT_COMMAND_BUTTON_CLICKED,(wxObjectEventFunction)&NoteDialog::OnInsertIndirectSource);
	Connect(ID_INSERT_KEYWORD,wxEVT_COMMAND_BUTTON_CLICKED,(wxObjectEventFunction)&NoteDialog::OnInsertKeyword);
	Connect(ID_MARK_KEYWORD,wxEVT_COMMAND_BUTTON_CLICKED,(wxObjectEventFunction)&NoteDialog::OnMarkKeyword);
	Connect(wxID_SAVE,wxEVT_COMMAND_BUTTON_CLICKED,(wxObjectEventFunction)&NoteDialog::OnSave);
	Connect(wxID_CANCEL,wxEVT_COMMAND_BUTTON_CLICKED,(wxObjectEventFunction)&NoteDialog::OnCancel);
	Connect(ID_RESET,wxEVT_COMMAND_BUTTON_CLICKED,(wxObjectEventFunction)&NoteDialog::OnReset);
	//*)

    caption_box->SetValue(note.caption);
    date_box->SetValue(note.date);
    manual_keywords_box->SetValue(note.manual_keywords);
    text_box->SetValue(note.text);
	fields_sizer->AddGrowableRow(2, 1);
	fields_sizer->AddGrowableRow(3, 4);

    // set text colors
    UpdateTextRange(0, text_box->GetLastPosition());

    // if we are editing a note focus the body
    if (!note.isEmpty())
        text_box->SetFocus();

    // set up kbg handler to allow TAB switching in multiline wxTextCtrl on GTK
    // see: http://trac.wxwidgets.org/ticket/12999
    // also see the event handler
    #ifdef TARGET_LINUX
    manual_keywords_box->Connect(wxEVT_CHAR, wxKeyEventHandler(NoteDialog::OnChildKeyDown), 0, this);
    text_box           ->Connect(wxEVT_CHAR, wxKeyEventHandler(NoteDialog::OnChildKeyDown), 0, this);
    #endif // TARGET_LINUX

	UpdateFonts();
}

NoteDialog::~NoteDialog()
{
	//(*Destroy(NoteDialog)
	//*)
}

static wxString StripEndingWhitespace(const wxString& arg)
{
    wxString result=arg;
    while (result.Len() > 0 && IsWhitespaceChar(result[result.Len()-1]))
        result.RemoveLast(1);
    return result;
}

void NoteDialog::OnDateModified(wxCommandEvent& event)
{
    if (CheckDate(date_box->GetValue())) {
        date_box->SetBackgroundColour(wxNullColour);
    } else {
        date_box->SetBackgroundColour(*wxRED);
    }
    date_box->Refresh();
    event.Skip();

}

void NoteDialog::OnKeywordsModified(wxCommandEvent& event)
{
}

int NoteDialog::getID() const
{
    return note.id;
}

bool NoteDialog::isModified() const
{
    if (note.caption != caption_box->GetValue())
        return true;
    if (note.date != date_box->GetValue())
        return true;
    if (note.manual_keywords != manual_keywords_box->GetValue())
        return true;
    if (note.text != StripEndingWhitespace(text_box->GetValue()))
        return true;
    return false;
}

void NoteDialog::UpdateFonts()
{
/*    wxFont f = config->GetFont(FONT_DEFAULT);
    caption_label   ->SetFont(f);
    caption_box     ->SetFont(f);
    date_label      ->SetFont(f);
    date_box        ->SetFont(f);
    keywords_label  ->SetFont(f);
    manual_keywords_box    ->SetFont(f);
    text_label      ->SetFont(f);
    text_box        ->SetFont(config->GetNoteFont());

    insert_source_button            ->SetFont(f);
    insert_indirect_source_button   ->SetFont(f);
    mark_keyword_button             ->SetFont(f);
    reset_button                    ->SetFont(f);
    save_button                     ->SetFont(f);
    cancel_button                   ->SetFont(f);

    top_sizer->Layout();
*/
}

void NoteDialog::OnInsertSource(wxCommandEvent& event)
{
    // if source is not specified, offer user to set it now
    if (top_frame->source == wxT("")) {
        if (!InputString(&top_frame->source, _("Source:"), _("Set Source name")))
            return;
    }

    // if preceding character isn't space, insert one before the source
    int cursor = text_box->GetInsertionPoint();
    if (cursor > 0) {
        wxString preceding = text_box->GetRange(cursor-1, cursor);
        if (preceding[0] != ' ' && preceding[0] != '\n' && preceding[0] != '\r')
            text_box->WriteText(" ");
    }

    // workaround problem on GTK: text style is explicitly changed to "current style" in wxGtkTextInsert().
    // This erases whatever style is set in EVT_TEXT callback handler.
    #ifdef TARGET_LINUX
    text_box->SetDefaultStyle(*wxBLUE);
    #endif

    // format and insert the source
    wxString source_formatted = wxT("(") + top_frame->source + wxT(", s. )");
    text_box->WriteText(source_formatted);
    text_box->SetInsertionPoint(text_box->GetInsertionPoint()-1);
    text_box->SetFocus();
}

void NoteDialog::OnInsertIndirectSource(wxCommandEvent& event)
{
    // let user specify indirect source first
    wxString indirect_source;
    if (!InputString(&indirect_source, _("Indirect source:"), _("Indirect source:")))
        return;

    // if direct source is not specified, offer user to set it now
    if (top_frame->source == wxT("")) {
        if (!InputString(&top_frame->source, _("Source:"), _("Direct source name not set")))
            return;
    }

    // if preceding character isn't space, insert one before the source
    int cursor = text_box->GetInsertionPoint();
    if (cursor > 0) {
        wxString preceding = text_box->GetRange(cursor-1, cursor);
        if (preceding[0] != ' ' && preceding[0] != '\n' && preceding[0] != '\r')
            text_box->WriteText(" ");
    }

    // workaround problem on GTK: text style is explicitly changed to "current style" in wxGtkTextInsert().
    // This erases whatever style is set in EVT_TEXT callback handler.
    #ifdef TARGET_LINUX
    text_box->SetDefaultStyle(*wxBLUE);
    #endif

    wxString source_formatted = wxT("(") + indirect_source + _("; podla ") + top_frame->source + _(", s. )");
    text_box->WriteText(source_formatted);
    text_box->SetInsertionPoint(text_box->GetInsertionPoint()-1);
    text_box->SetFocus();
}

static void SortKeywordsByLastUsed(vector<Keyword>& keywords)
{
    wxASSERT(keywords.size() > 0);
    if (keywords.size() == 1)
        return;

    // simple select sort, speed doesn't matter here
    for (unsigned x=0; x<keywords.size(); x++) {

        long top_val = keywords[x].used;
        unsigned top_index = x;
        for (unsigned y=x+1; y<keywords.size(); y++) {
            if (keywords[y].used > top_val) {
                top_val = keywords[y].used;
                top_index = y;
            }
        }

        if (top_index != x) {
            Keyword k = keywords[x];
            keywords[x] = keywords[top_index];
            keywords[top_index] = k;
        }
    }
}

void NoteDialog::OnMarkKeyword(wxCommandEvent& event)
{
    dbg("NoteDialog::OnMarkKeyword()\n");

    // can't use GetInsertionPoint() to address string returned by GetValue()!
    // prior counts CRLF, latter only LF
    // extract string up to position
    // TODO: limit number of chars extracted
    int cursor_pos = text_box->GetInsertionPoint();
    wxString text = text_box->GetRange(0, cursor_pos);
    int pos = text.Len();

    // skip whitespace preceding cursor
    int whitespace_end = pos;
    while (pos>0 && IsBlankChar(text[pos-1])) pos--;
    if (pos==0 || !IsKeywordChar(text[pos-1])) return;
    int whitespace_size = whitespace_end - pos;             // save whitespace size, in order to delete it later
    int word_end = pos;

    // find beginning of previous 1 to 3 keywords
    wxString preceding_words[3];
    for (int word_count=0; word_count<3; word_count++) {
        while (pos>0 && IsBlankChar(text[pos-1])) pos--;
        int prev_word_start = pos;
        while (pos>0 && IsKeywordChar(text[pos-1])) pos--;
        int word_start = pos;
        if (word_start == prev_word_start) break;
        preceding_words[word_count] = text.Mid(word_start, word_end-word_start);
    }

    // try from longest to shortest variant
    bool found=false;
    for (int word_count=3; word_count>0; word_count--) {
        wxString word = preceding_words[word_count-1];
        if (word.IsEmpty())
            continue;

        // look up all keywords whose hints can produce our word(s)
        dbg("NoteDialog::OnMarkKeyword() lookup '%s'\n", word);
        vector<Keyword> keywords;
        if (!keyword_list.KeywordsByHint(keywords, word))
            continue;
        found=true;
        dbg("NoteDialog::OnMarkKeyword() lookup done\n");

        // sort them according to last used time
        SortKeywordsByLastUsed(keywords);

        // let user select keyword (if there are multiple)
        int selected_keyword_index = 0;
        if (keywords.size()>1) {
            wxArrayString keyword_names;
            for (int x=0; x<(int)keywords.size(); x++) keyword_names.Add(keywords[x].sortname);
            wxString msg = wxFormat(_("Select keyword for word '%s'"), word);
            selected_keyword_index = wxGetSingleChoiceIndex(msg, _("Insert keyword"), keyword_names);
            if (selected_keyword_index == -1)
                continue;
        }

        // update keyword last-used timestamp
        top_frame->KeywordUsed(keywords[selected_keyword_index].id);

        // workaround problem on GTK: text style is explicitly changed to "current style" in wxGtkTextInsert().
        // This erases whatever style is set in EVT_TEXT callback handler.
        #ifdef TARGET_LINUX
        text_box->SetDefaultStyle(wxColor(255,130,130));
        #endif

        // use {{...}} if word is identical to keyword
        // TODO: disabled for now (this feature would break original wording if keyword renaming is supported later)
        if (0 && word == keywords[selected_keyword_index].name) {
            wxString text = wxFormat("{{%s}}", word);
            text_box->Remove(cursor_pos - whitespace_size - word.Len(), cursor_pos);
            text_box->WriteText(text);
        }

        // delete all whitespace and insert nicely formatted note
        else {
            wxString text = " {";
            for (int x=0; x<word_count; x++) text += '<';
            text += wxFormat("%s}", keywords[selected_keyword_index].name);
            text_box->Remove(cursor_pos - whitespace_size, cursor_pos);
            text_box->WriteText(text);
        }
        text_box->SetFocus();

        found=true;
        break;
    }

    if (!found)
        ErrMsg(_("None of following hints found:\n\t%s\n\t%s\n\t%s"),
                preceding_words[0], preceding_words[1], preceding_words[2]);


    dbg("NoteDialog::OnMarkKeyword() done\n");
}

void NoteDialog::OnCancel(wxCommandEvent& event)
{
    top_frame->CloseTab();
}

void NoteDialog::OnSave(wxCommandEvent& event)
{
    // check for Unicode combining chars (we don't support them)
    // do this first, if combining char is root of problem, report it instead of refusing it as "incorrect format"
    #define CHECK_UNICODE(x) if(wxString_CheckCombining((x)->GetValue())) {(x)->SetFocus(); return; }
    CHECK_UNICODE(caption_box);
    CHECK_UNICODE(date_box);
    CHECK_UNICODE(manual_keywords_box);
    CHECK_UNICODE(text_box);

    // check format of input
    wxString text = StripEndingWhitespace(text_box->GetValue());
    if (!CheckDate(date_box->GetValue())) {
        wxMessageBox(_("Date format is incorrect"), _("Warning"));
        date_box->SetFocus();
        return;
    }
    if (!CheckKeywordsString(manual_keywords_box->GetValue())) {
        wxMessageBox(_("Manual keywords format is incorrect."), _("Warning"));
        manual_keywords_box->SetFocus();
        return;
    }
    if (text.Len() == 0) {
        wxMessageBox(_("Note with empty text is not allowed."), _("Warning"));
        text_box->SetFocus();
        return;
    }

    // warn if captions seems like date
    const wxString caption = caption_box->GetValue();
    if (!caption.IsEmpty() && date_box->GetValue().IsEmpty() && CheckDate(caption)) {
        if (!Confirm(_("Caption seems to contain date.\nYou may have entered date into wrong field.\nSave anyway?"))) {
            caption_box->SetFocus();
            return;
        }
    }

    // Save note if it was changed
    if (isModified()) {
        note.caption         = caption_box->GetValue();
        note.date            = date_box->GetValue();
        note.manual_keywords = manual_keywords_box->GetValue();
        note.text            = text;
        note.modified        = wxDateTime::GetTimeNow();
        if (note.id == 0) {
            wxASSERT(note.created == 0);
            note.created = note.modified;
        }
        top_frame->SaveNote(note);
    }

    // Close this tab
    top_frame->CloseTab();
}

void NoteDialog::OnReset(wxCommandEvent& event)
{
    if (isModified()) {
        if (!Confirm(_("Note was modified.\nReset anyway?")))
            return;
    }
    caption_box->SetValue(note.caption);
    date_box->SetValue(note.date);
    manual_keywords_box->SetValue(note.manual_keywords);
    text_box->SetValue(note.text);
}

void NoteDialog::OnInsertKeyword(wxCommandEvent& event)
{
    wxString keyword;

    // get keyword
reenter:
    if (!InputString(&keyword, _("Keyword:"), _("Insert explicit keyword")))
        return;
    if (!CheckKeyword(keyword)) {
        ErrMsg(_("Invalid keyword '%s'"), keyword);
        goto reenter;
    }

    // if preceding character is alphanumeric, insert space
    int cursor = text_box->GetInsertionPoint();
    if (cursor > 0) {
        wxString preceding = text_box->GetRange(cursor-1, cursor);
        if (IsAlphabetic(preceding[0]))
            text_box->WriteText(" ");
    }

    // workaround problem on GTK: text style is explicitly changed to "current style" in wxGtkTextInsert().
    // This erases whatever style is set in EVT_TEXT callback handler.
    #ifdef TARGET_LINUX
    text_box->SetDefaultStyle(wxColor(255,130,130));
    #endif

    keyword.Prepend("{{");
    keyword.Append("}}");
    text_box->WriteText(keyword);
    text_box->SetFocus();
}

void NoteDialog::UpdateTextRange(int from, int to)
{
    int len = text_box->GetLastPosition();
    wxASSERT(from >= 0 && from <= to && to <= len);

    //TODO: steals cursor under Linux?
#ifdef TARGET_WIN32
    text_box->Freeze();
    if (updating_text_style) return;
    updating_text_style = true;
#endif

    // find beginning of line in which 'from' lies
    wxString chunk;
    int chunk_base;
    while(from > 0) {
        chunk_base = max(0, from-200);
        chunk = text_box->GetRange(chunk_base, from);
        for (int x=(int)chunk.Len()-1; x>=0; x--)
            if (chunk[x]=='\r' || chunk[x]=='\n') {
                from = chunk_base + x + 1;
                goto start_found;
            }
        from = chunk_base;
    }
    start_found:

    // find end of line in which 'to' lies
    int chunk_end;
    while (to < len) {
        chunk_end = min(len, to+200);
        chunk = text_box->GetRange(to, chunk_end);
        for (int x=0; x<(int)chunk.Len(); x++)
            if (chunk[x]=='\r' || chunk[x]=='\n') {
                to = to + x;
                goto end_found;
            }
        to = chunk_end;
    }
    end_found:

    bool in_keyword=false, in_inline_keyword=false;
    int in_parent=0, parent_start=0, keyword_start=0, last_end=0;
    wxString text = text_box->GetRange(from, to);
    for (int x=0; x<(int)text.Len(); x++) {
        switch((wxChar)text[x]) {

        case '(':
            if (in_keyword || in_parent)
                break;
            in_parent = true;
            parent_start = x;
            break;

        case ')':
            if (in_parent) {
                if (TextLayout::IsSourceParenthesis(text, parent_start, x+1)) {
                    //dbg("- source parenthesis at %d-%d\n", from+parent_start, from+x);
                    text_box->SetStyle(from+last_end, from+parent_start, *wxBLACK);
                    text_box->SetStyle(from+parent_start, from+x+1, *wxBLUE);
                    last_end = x+1;
                }
            }
            in_parent = false;
            break;

        case '{':
            in_parent = false;

            // already inside keyword?
            if (in_keyword) {
                //dbg("- keyword mark {%s at %d was invalid\n", in_inline_keyword?"{":"", keyword_start);
                int keyword_mark_size = in_inline_keyword ? 2:1;
                text_box->SetStyle(from+last_end, from+keyword_start, *wxBLACK);                        // text before previous '{' (or '{{')
                text_box->SetStyle(from+keyword_start, from+keyword_start+keyword_mark_size, *wxRED);   // previous '{' (or '{{')
                text_box->SetStyle(from+keyword_start+keyword_mark_size, from+x, *wxBLACK);             // text between previous and this '{' (or '{{')
                last_end = x+keyword_mark_size;
                in_inline_keyword = false;
            }

            // start of keyword
            in_keyword = true;
            keyword_start = x;

            // is this inline keyword? (starts with "{{")
            if (x+1<(int)text.Len() && text[x+1]=='{') {
                in_inline_keyword = true;
                x++;
            }

            //dbg("- keyword mark {%s at %d\n", in_inline_keyword?"{":"", keyword_start);
            break;
        case '}':
            in_parent = false;

            if (in_keyword) {
                if (in_inline_keyword && !(x+1 < (int)text.Len() && text[x+1]=='}')) {
                    //dbg("invalid {{-} keyword at %d-%d\n", keyword_start, x);
                    text_box->SetStyle(from+last_end, from+keyword_start, *wxBLACK);          // text before '{'
                    text_box->SetStyle(from+keyword_start, from+x+1, *wxRED);                 // text between '{{' and '}'
                    last_end = x+1;
                    in_keyword = false;
                    in_inline_keyword = false;
                    break;
                }
                //dbg("- %skeyword at %d-%d\n", in_inline_keyword?"inline ":"", keyword_start, x+in_inline_keyword);
                text_box->SetStyle(from+last_end, from+keyword_start, *wxBLACK);               // text before '{'
                text_box->SetStyle(from+keyword_start, from+x+1+in_inline_keyword, wxColor(255,130,130));  // text between '{' and '}' (or '{{' and '}}')

                // mark disallowed keyword characters
                bool starting = true;
                for (int charpos=keyword_start+1+in_inline_keyword; charpos<x; charpos++) {
                    wxChar c = text[charpos];
                    if (starting && c!='<')
                        starting = false;

                    if (!( IsKeywordChar(text[charpos]) || (starting && c=='<') ))
                        text_box->SetStyle(from+charpos, from+charpos+1, *wxRED);
                }

                if (in_inline_keyword) x++;
                last_end = x+1;
                in_keyword = false;
                in_inline_keyword = false;
            } else {
                //dbg("invalid } at at %d\n", x);
                text_box->SetStyle(from+last_end, from+x, *wxBLACK);
                text_box->SetStyle(from+x, from+x+1, *wxRED);
                last_end = x+1;
            }

            break;
        }

        if (in_inline_keyword) {
            wxASSERT(in_keyword);
        }
        wxASSERT(!(in_keyword && in_parent));
    }

    if (in_keyword) {
        int keyword_mark_size = in_inline_keyword ? 2:1;
        text_box->SetStyle(from+last_end, from+keyword_start, *wxBLACK);
        text_box->SetStyle(from+keyword_start, from+keyword_start+keyword_mark_size, *wxRED);
        last_end = keyword_start+keyword_mark_size;
    }
    text_box->SetStyle(from+last_end, to, *wxBLACK);

    text_box->Thaw();
    updating_text_style = false;
}

void NoteDialog::OnTextChanged(wxCommandEvent& event)
{
    int lastlen = this->last_text_len;
    int len = text_box->GetLastPosition();
    int ins = text_box->GetInsertionPoint();

    // try to determine what sort of operation was performed on this text, and update text
    // style only in part that was changed
    // TODO: this is dirty way to do. Instead, we should subclass wxTextCtrl and
    //       handle certain messages ourselves to see what's happening

    // UpdateTextRange on Windows calls this callback
    // we need to ignore events caused by it
    if (updating_text_style)
        return;

    dbg("len = %d, lastlen = %d, ins = %d\n", len, lastlen, ins);
    dbg("\n\n%s\n\n", text_box->GetValue().wx_str());

    // update last text len to current (so we can return from anywhere after this)
    this->last_text_len = len;

    // initial empty text initialization
    if (len == 0 && lastlen == 0) {
    }

    // text deleted
    else if (len < lastlen) {
        UpdateTextRange(ins, ins);
    }

    // text inserted
    else if (len > lastlen) {
        int inserted = len - lastlen;
        //dbg("ins=%d, inserted=%d, len=%d, lastlen=%d\n", ins, inserted, len, lastlen);

        // initial SetValue called
        if (ins==0 && lastlen==0 && inserted==len) {
            UpdateTextRange(0, len);
        }

        // single char typed
        // (previously I had optimized handling for typing '{', '}', etc., but that would be too complex with inline keywords.
        // UpdateTextRange() seems to be fast enough anyway.)
        else if (inserted == 1) {
            UpdateTextRange(ins-1, ins);
        }

        // pasted text
        else {
            wxASSERT(ins >= inserted);
            UpdateTextRange(ins-inserted, ins);
            text_box->Refresh();
        }
    }

    // other operation on text
    // for example if you replace N characters with same number of characters
    // no way to determine, so update entire text
    else {
        UpdateTextRange(0, len);
    }
}


#ifdef TARGET_LINUX
/// Allow Tab traversal over GTK wxTextCtrl with just Tab, not Ctrl+Tab
/// See http://trac.wxwidgets.org/ticket/12999
// TODO: overload wxTextCtrl (or extend it?) using something like following:
// http://stackoverflow.com/questions/1769657/how-to-set-call-back-for-enter-key-of-gtktextview
void NoteDialog::OnChildKeyDown(wxKeyEvent& event)
{
    dbg("NoteDialog::OnChildKeyDown(), keycode=%u, ctrl=%u\n", event.GetKeyCode(), event.ControlDown());

    // if this is just Tab, navigate to next control
    if (event.GetKeyCode() == WXK_TAB && !event.ControlDown()) {

        // TODO: it seems kinda hard to force native tab traversal, or to find next tab traversal window
        // ourselves, so just hardcode it for now.
        wxWindow* cur_win = (wxWindow*)event.GetEventObject();
        if (cur_win == manual_keywords_box) {
            if (event.ShiftDown())
                date_box->SetFocus();
            else
                text_box->SetFocus();
        } else if (cur_win == text_box) {
            if (event.ShiftDown())
                manual_keywords_box->SetFocus();
            else
                insert_source_button->SetFocus();
        }
        return;
    }

    // Since wx3.0 multiline wxTextCtrl steal CTRL+PageUp and CTRL+PageDown.
    // We use them here, so don't let him get it.
    if (event.ControlDown()) {
        if (event.GetKeyCode() == WXK_PAGEDOWN) {
            top_frame->NextTab();
            return;
        }
        if (event.GetKeyCode() == WXK_PAGEUP) {
            top_frame->PrevTab();
            return;
        }
    }

    event.Skip();
}
#endif
